/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.bulkimport.impl;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.alfresco.repo.bulkimport.BulkFSImportEvent;
import org.alfresco.repo.bulkimport.BulkFilesystemImporter;
import org.alfresco.repo.bulkimport.BulkImportParameters;
import org.alfresco.repo.bulkimport.BulkImportStatus;
import org.alfresco.repo.bulkimport.DirectoryAnalyser;
import org.alfresco.repo.bulkimport.NodeImporter;
import org.alfresco.repo.lock.JobLockService;
import org.alfresco.repo.lock.LockAcquisitionException;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.rule.RuleService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * 
 * @since 4.0
 *
 */
public abstract class AbstractBulkFilesystemImporter implements BulkFilesystemImporter, InitializingBean, ApplicationContextAware
{
    private static final QName LOCK = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "BatchFilesystemImport");
    protected static final Log logger = LogFactory.getLog(BulkFilesystemImporter.class);

    protected ApplicationContext applicationContext;
    
    protected FileFolderService fileFolderService;
    protected TransactionService transactionService;
    protected PermissionService permissionService;
    protected RetryingTransactionHelper transactionHelper;
    protected RuleService ruleService;

    protected BulkImportStatusImpl importStatus;
    protected DirectoryAnalyser directoryAnalyser = null;

    protected JobLockService jobLockService;
    
    protected BehaviourFilter behaviourFilter;
    
	public void setRuleService(RuleService ruleService)
	{
		this.ruleService = ruleService;
	}

	public void setBehaviourFilter(BehaviourFilter behaviourFilter)
	{
		this.behaviourFilter = behaviourFilter;
	}
	
	public void setJobLockService(JobLockService jobLockService)
	{
		this.jobLockService = jobLockService;
	}
    
	public void setImportStatus(BulkImportStatusImpl importStatus)
	{
		this.importStatus = importStatus;
	}
	
    public final void setDirectoryAnalyser(DirectoryAnalyser directoryAnalyser)
    {
        this.directoryAnalyser = directoryAnalyser;
    }

	public void setFileFolderService(FileFolderService fileFolderService)
	{
		this.fileFolderService = fileFolderService;
	}

	public void setTransactionService(TransactionService transactionService)
	{
		this.transactionService = transactionService;
	}

	public void setPermissionService(PermissionService permissionService)
	{
		this.permissionService = permissionService;
	}
    
    /**
     * @see org.alfresco.repo.bulkimport.BulkFilesystemImporter#getStatus()
     */
    public final BulkImportStatus getStatus()
    {
        return(importStatus);
    }
	
	public void afterPropertiesSet() throws Exception
	{
        PropertyCheck.mandatory(this, "fileFolderService", fileFolderService);
        PropertyCheck.mandatory(this, "transactionService", transactionService);
        PropertyCheck.mandatory(this, "permissionService", permissionService);
        
        PropertyCheck.mandatory(this, "importStatus", importStatus);
        PropertyCheck.mandatory(this, "directoryAnalyser", directoryAnalyser);
        
        this.transactionHelper = transactionService.getRetryingTransactionHelper();
	}

    protected abstract void bulkImportImpl(BulkImportParameters bulkImportParameters, NodeImporter nodeImporter, String lockToken);

    /**
     * Attempts to get the lock. If the lock couldn't be taken, then <tt>null</tt> is returned.
     * 
     * @return Returns the lock token or <tt>null</tt>
     */
    protected String getLock(long time)
    {
        try
        {
            return jobLockService.getLock(LOCK, time);
        }
        catch (LockAcquisitionException e)
        {
            return null;
        }
    }

    protected void refreshLock(String lockToken, long time)
    {
        if (lockToken == null)
        {
            throw new IllegalArgumentException("Must provide existing lockToken");
        }
        jobLockService.refreshLock(lockToken, LOCK, time);
    }
    
    protected void releaseLock(String lockToken)
    {
        if (lockToken == null)
        {
            throw new IllegalArgumentException("Must provide existing lockToken");
        }
        jobLockService.releaseLock(lockToken, LOCK);
    }
    
    /*
     * Because commons-lang ToStringBuilder doesn't seem to like unmodifiable Maps
     */
    protected final String mapToString(Map<?, ?> map)
    {
        StringBuffer result = new StringBuffer();
        
        if (map != null)
        {
            result.append('[');

            if (map.size() > 0)
            {
                for (Object key : map.keySet())
                {
                    result.append(String.valueOf(key));
                    result.append(" = ");
                    result.append(String.valueOf(map.get(key)));
                    result.append(",\n");
                }
                
                // Delete final dangling ", " value
                result.delete(result.length() - 2, result.length());
            }
            
            result.append(']');
        }
        else
        {
            result.append("(null)");
        }
        
        return(result.toString());
    }
    
    protected final String getRepositoryPath(NodeRef nodeRef)
    {
        String result = null;
        
        if (nodeRef != null)
        {
            List<FileInfo> pathElements = null;
            
            try
            {
                pathElements = fileFolderService.getNamePath(null, nodeRef);

                if (pathElements != null && pathElements.size() > 0)
                {
                    StringBuilder temp = new StringBuilder();
                    
                    for (FileInfo pathElement : pathElements)
                    {
                        temp.append("/");
                        temp.append(pathElement.getName());
                    }
                    
                    result = temp.toString();
                }
            }
            catch (final FileNotFoundException fnfe)
            {
                // Do nothing
            }
        }
        
        return(result);
    }

    protected final void validateNodeRefIsWritableSpace(NodeRef target)
    {
        if (target == null)
        {
            throw new IllegalArgumentException("target must not be null.");
        }
        
        if (!fileFolderService.exists(target))
        {
            throw new IllegalArgumentException("Target '" + target.toString() + "' doesn't exist.");
        }
        
        if (AccessStatus.DENIED.equals(permissionService.hasPermission(target, PermissionService.ADD_CHILDREN)))
        {
            throw new IllegalArgumentException("Target '" + target.toString() + "' is not writeable.");
        }
        
        if (!fileFolderService.getFileInfo(target).isFolder())
        {
            throw new IllegalArgumentException("Target '" + target.toString() + "' is not a space.");
        }
    }
    
    protected String getFileName(File file)
    {
    	return FileUtils.getFileName(file);
    }
    
    protected String getLockToken()
    {
		// Take out a bulk filesystem import lock
		RetryingTransactionCallback<String> txnWork = new RetryingTransactionCallback<String>()
        {
            public String execute() throws Exception
            {
		        String lockToken = getLock(20000L);
		        return lockToken;
            }
        };

        String lockToken = transactionService.getRetryingTransactionHelper().doInTransaction(txnWork, false, true);
//        if(lockToken == null)
//        {
//            logger.warn("Can't get lock. Assume multiple bulk filesystem importers ...");
//            return;
//        }
        
        return lockToken;
    }
    
    public void validateSourceIsReadableDirectory(File source)
    {
        try
        {
            if (source == null)
            {
                throw new IllegalArgumentException("source must not be null.");
            }
            
            if (!source.exists())
            {
                throw new IllegalArgumentException("Source '" + source.getCanonicalPath() + "' doesn't exist.");
            }
            
            if (!source.canRead())
            {
                throw new IllegalArgumentException("Source '" + source.getCanonicalPath() + "' is not readable.");
            }
            
            if (!source.isDirectory())
            {
                throw new IllegalArgumentException("Source '" + source.getCanonicalPath() + "' is not a directory.");
            }
        }
        catch (final IOException ioe)
        {
            throw new RuntimeException(ioe);
        }
    }
    
    public void asyncBulkImport(final BulkImportParameters bulkImportParameters, final NodeImporter nodeImporter)
    {
    	final String currentUser = AuthenticationUtil.getFullyAuthenticatedUser();

        Runnable backgroundLogic = new Runnable()
        {
            public void run()
            {
            	AuthenticationUtil.runAs(new RunAsWork<Object>()
            	{
            		public Object doWork()
            		{
            			bulkImport(bulkImportParameters, nodeImporter);
		            	return null;
					}
				}, currentUser);
            }
        };

        Thread backgroundThread = new Thread(backgroundLogic, "BulkFilesystemImport-BackgroundThread");
        backgroundThread.start();
    }
    
    /**
     * @see org.alfresco.repo.bulkimport.BulkFilesystemImporter#bulkImport(org.alfresco.repo.bulkimport.BulkImportParameters, org.alfresco.repo.bulkimport.NodeImporter)
     */
    public void bulkImport(final BulkImportParameters bulkImportParameters, final NodeImporter nodeImporter)
    {
    	final File sourceFolder = nodeImporter.getSourceFolder();
    	final BulkFilesystemImporter importer = this;

    	transactionHelper.doInTransaction(new RetryingTransactionCallback<Void>()
    	{
    		@Override
    		public Void execute() throws Throwable
    		{
    		    final String sourceDirectory = getFileName(sourceFolder);
    		    final String targetSpace = getRepositoryPath(bulkImportParameters.getTarget());
    		    final String lockToken = getLockToken();

    		    try
    		    {
    		        importStatus.startImport(sourceDirectory, targetSpace);

    		        BulkFSImportEvent bulkImportEvent = new BulkFSImportEvent(importer);
    		        applicationContext.publishEvent(bulkImportEvent);

    		        validateNodeRefIsWritableSpace(bulkImportParameters.getTarget());
    		        validateSourceIsReadableDirectory(sourceFolder);

    		        if(logger.isDebugEnabled())
    		        {
    		            logger.debug("Bulk import started from '" + sourceFolder.getAbsolutePath() + "'...");
    		        }


    		        bulkImportImpl(bulkImportParameters, nodeImporter, lockToken);

    		        importStatus.stopImport();

    		        if(logger.isDebugEnabled())
    		        {
    		            logger.debug("Bulk import from '" + getFileName(sourceFolder) + "' succeeded.");
    		        }

    		        return null;
    		    }
    		    catch(Throwable e)
    		    {
    		        importStatus.stopImport(e);
    		        throw e;
    		    }
    		    finally
    		    {
    		        BulkFSImportEvent bulkImportEvent = new BulkFSImportEvent(importer);
    		        applicationContext.publishEvent(bulkImportEvent);

    		        releaseLock(lockToken);
    		    }
    		}
    	}, false, true);
    }
        
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
	{
		this.applicationContext = applicationContext;
	}
}
